<?php


namespace app\api\ctrl;


use app\service\Predis;
use app\service\RedisLock;
use app\service\RedisMutexLock;


/**
 * 高并发加锁试验
 * Class LimitCtrl
 * @package app\api\ctrl
 */
class LockCtrl extends BaseCtrl
{
    /**
     * 分布式 乐观锁/悲观锁 [压测ok]
     * ab -n500 -c100 http://127.0.0.1:82/api/lock/tryGetDistributedLock
     * ab -n500 -c100 http://120.25.227.106:8082/api/lock/tryGetDistributedLock
     * @return bool
     */
    public function tryGetDistributedLock() {
        //对每个请求生成唯一性的id
        $requestId = md5(uniqid(mt_rand(), true)) . rand(10000, 99999);
        $client_id = session_create_id();
        $lockKey = 'stock_1:lock';
        $stock_key = 'stock_1';
        $limit = 1; // 抢购限制数量

        try {
            // 加锁[推荐使用悲观锁], 加锁也需要放在try里面
            $lockObj = new RedisMutexLock();
            $lock = $lockObj->tryGetDistributedLock($lockKey, $requestId, 3, 1);
            if (!$lock) {
                debugLog("$requestId: 获取锁失败", 'redis');
                return false;
            }
            debugLog("$requestId: 获取锁成功", 'redis');

            // 处理业务
            $stock = Predis::getInstance()->get($stock_key);
            if ($stock < $limit) {
                debugLog("$requestId: 库存不足，剩余：$stock", 'redis');
                return false;
            }

            // redis减库存
            $n = Predis::getInstance()->decrBy($stock_key, $limit);
            debugLog("$requestId: 减redis库存成功，剩余：$n", 'redis');

            // db减库存
            $goodsBus = new \app\common\business\Goods();
            $ret = $goodsBus->decrById(14, 'stock', $limit);
            if (!$ret) {
                debugLog("$requestId: 减db库存失败", 'redis');
                Predis::getInstance()->incrBy($stock_key, $limit);
                return false;
            }
            debugLog("$requestId: 减db库存成功", 'redis');
            // 写入订单队列
            $order = [
                'uid' => $requestId,
                'num' => $limit,
            ];
            Predis::getInstance()->lpush($stock_key . '_result', json_encode($order));
            usleep(1000);
        } catch (\Exception $e) {
            // 异常处理
            debugLog("$requestId: " . $e->getMessage(), 'error');
        } finally {
            // 释放锁
            $result = $lockObj->releaseDistributedLock($lockKey, $requestId);
            $msg = $result ? 'success' : 'fail';
            debugLog("$requestId: 释放锁{$msg}", 'redis');
        }
        return true;
    }

    /**
     * redis watch 的乐观锁方案
     * @return bool|string
     */
    public function redis_watch()
    {
        $redis_stock = Predis::getInstance()->get("stock_1");
        if (empty($redis_stock) && $redis_stock == 0) {
            return "商品已被抢空";
        }

        $user_id = mt_rand(1, 999);
        $redis_list = Predis::getInstance()->lRange("user_list", 0, -1);
        // 限定只抢购一次
        if ($redis_list && in_array($user_id, $redis_list)) {
            return "您已经抢购过啦,用户id:" . $user_id;
        }
        Predis::getInstance()->lPush("user_list", $user_id);

        Predis::getInstance()->Watch("stock_1"); //乐观锁 监视是否被其它的进程更改
        Predis::getInstance()->Multi(); // 开启事务
        Predis::getInstance()->decr("stock_1");
        $is_ok = Predis::getInstance()->exec(); // 执行以上redis事务 成功1 失败0
        if ($is_ok) {
            $user_id = Predis::getInstance()->rPop("user_list");
            // 开启事务 Todo
            try {
                $data = [
                    "user_id" => $user_id,
                    "orders_num" => time() . mt_rand(10, 999),
                ];
                //$res = DB::table("test_table")->lockForUpdate()->insert($data);
                echo "抢购成功,用户id:" . $user_id;
                return true;
            } catch (\Exception $e) {
                Predis::getInstance()->Discard();
                echo "抢购失败,用户id:" . $user_id . "," . $e->getMessage();
            }
        } else {
            echo "商品已被抢空,用户id:" . $user_id;
            Predis::getInstance()->Discard();
        }

        return false;
    }

    /**
     * list
     * @return bool|string
     */
    public function redis_list()
    {
        $goods_id = Predis::getInstance()->lPop('stock_1');
        if ($goods_id == false) {
            return '秒杀结束，没有商品库存了';
        } else {
            $res = Predis::getInstance()->lPush('goods_result:1', $goods_id);
            if ($res == false) {
                // 返还库存
                Predis::getInstance()->rpush('stock_1', 1);
                echo "writelog:" . $goods_id;
            } else {
                echo "success" . $goods_id;
                // TODO 更新db
            }
        }
        return true;
    }

    /**
     * lua decr
     * ab -n500 -c100 http://127.0.0.1:82/api/lock/redis_lua
     * @return string|void
     */
    public function redis_lua()
    {
        $limit = 1; //抢购数量
        $user_id = mt_rand(1, 9999);
        $stock_key = 'stock_1';
        $redis_stock = Predis::getInstance()->get($stock_key);
        if (!$redis_stock || $redis_stock < $limit) {
            debugLog("$user_id: 商品已被抢空", 'redis');
            return "商品已被抢空!";
        }

        $redis_list = Predis::getInstance()->lRange("stock_1_user", 0, -1);
        // 限定只抢购一次
        if ($redis_list && in_array($user_id, $redis_list)) {
            debugLog("$user_id: 您已经抢购过啦", 'redis');
            return "您已经抢购过啦，用户id:" . $user_id;
        }
        Predis::getInstance()->lPush("stock_1_user", $user_id);

        // lua脚本
        $script = <<<EOF
if redis.call("get", KEYS[1]) >= ARGV[1] then
    return redis.call("decrby", KEYS[1], ARGV[1])
else
    return false
end
EOF;
        $res = Predis::getInstance()->eval($script, [$stock_key, $limit], 1);
        if ($res) {
            debugLog("$user_id: redis减库存成功, 剩余：$res", 'redis');
            Predis::getInstance()->lPush("stock_1_result", $user_id);

            // db减库存
            $goodsBus = new \app\common\business\Goods();
            $ret = $goodsBus->decrById(14, 'stock', $limit);
            if ($ret) {
                debugLog("$user_id: db减库存成功", 'redis');
                return true;
            }
            debugLog("$user_id: db减库存失败", 'redis');
            Predis::getInstance()->incrby($stock_key, $limit);
        } else {
            debugLog("$user_id: 人太多请重试", 'redis');
        }

        return false;
    }
}